<?php

namespace app\service\modbus;

use EchoLogger;
use Exception;
use ModbusTcpClient\Composer\Read\ReadRegistersBuilder;
use ModbusTcpClient\Composer\Write\WriteRegistersBuilder;
use ModbusTcpClient\Network\BinaryStreamConnection;
use ModbusTcpClient\Network\NonBlockingClient;
use ModbusTcpClient\Packet\ModbusFunction\ReadHoldingRegistersRequest;
use ModbusTcpClient\Packet\ModbusFunction\ReadHoldingRegistersResponse;
use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleRegistersRequest;
use ModbusTcpClient\Packet\ResponseFactory;
use ModbusTcpClient\Packet\RtuConverter;
use ModbusTcpClient\Utils\Endian;
use ModbusTcpClient\Utils\Packet;
use support\Log;
use support\Response;

class ModbusTcpService
{
    protected $connection;
    protected $host;
    protected $port;

    public function __construct($host = '127.0.0.1', $port = 502)
    {
        $this->connection = BinaryStreamConnection::getBuilder()
            ->setPort($port)
            ->setHost($host)
            ->setConnectTimeoutSec(1.5) // timeout when establishing connection to the server
            ->setWriteTimeoutSec(0.5) // timeout when writing/sending packet to the server
            ->setReadTimeoutSec(0.3) // timeout when waiting response from server
            ->build();
        $this->host = $host;
        $this->port = $port;
    }

    /**
     * 判断modbus寄存器是否在线
     * @return string
     */
    public function isOnline(): string
    {
        $connection = $this->connection;
        try {
            $connection->connect();
            $is_online = "online";
        } catch (\Exception $exception) {
            $is_online = "offline";
        } finally {
            $connection->close();
        }
        return $is_online;
    }

    /**
     * 获取modbus-tcp的值
     * @param int $quantity 数量
     * @param int $unitID 站号
     * @param int $start_point 起始地址
     * @param int $end_point 结束地址
     * @return array|int
     */
    public function getModbusTcpValue(int $unitID = 1, int $start_point = 0, int $end_point = 10, int $quantity = 1)
    {
        Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST; // set default (global) endian used for parsing data
        $connection = $this->connection;
        $data = [];
        if ($start_point == $end_point) {
            $packet = new ReadHoldingRegistersRequest($start_point, $quantity, $unitID); // NB: This is Modbus TCP packet not Modbus RTU over TCP!
            try {
                $binaryData = $connection->connect()->sendAndReceive($packet);
                /**
                 * @var $response ReadHoldingRegistersResponse
                 */
                $response = ResponseFactory::parseResponseOrThrow($binaryData);
                $value = 0;
                foreach ($response as $word) {
                    $value = $word->getBytes()[1];
                }
                return $value;
            } catch (Exception $exception) {
                Log::error('An exception occurred' . $exception->getMessage());
                $data['error'] = 'An exception occurred' . $exception->getMessage() . $exception->getTraceAsString();
            } finally {
                $connection->close();
            }
        } else {
            for ($i = $start_point; $i <= $end_point; $i++) {
                $startAddress = $i;
                $packet = new ReadHoldingRegistersRequest($startAddress, $quantity, $unitID); // NB: This is Modbus TCP packet not Modbus RTU over TCP!
                try {
                    $binaryData = $connection->connect()->sendAndReceive($packet);
                    /**
                     * @var $response ReadHoldingRegistersResponse
                     */
                    $response = ResponseFactory::parseResponseOrThrow($binaryData);
                    foreach ($response as $word) {
                        $data[] = [
                            'point' => $i,
                            'value' => $word->getBytes()[1]
                        ];
                    }
                } catch (Exception $exception) {
                    Log::error('An exception occurred' . $exception->getMessage());
                    $data['error'] = 'An exception occurred' . $exception->getMessage() . $exception->getTraceAsString();
                } finally {
                    $connection->close();
                }
            }
        }

        return $data;
    }

    /**
     * 设置modbus-tcp的值
     * @param int $point 地址
     * @param int $value 值
     * @param int $unitId 站号
     * @return array|Response
     */
    public function setModbusTcpValue(int $point = 0, int $value = 0, int $unitId = 1)
    {
        $connection = $this->connection;
        $data_value = modbus_data_type($value);
        if (empty($data_value['value'])) {
            return errorMessage("数据类型错误");
        }
        if ($data_value['error']) {
            return errorMessage($data_value['error']);
        }
        $v = $data_value['value'];
        $binary_data = "";
        try {
            $packet1 = new WriteMultipleRegistersRequest($point, [$v], $unitId);
            $binaryData = $connection->connect()->sendAndReceive($packet1);
            //unpack('H*', $binaryData)[1])
            $binary_data = unpack('H*', $binaryData)[1];
        } catch (Exception $exception) {
            Log::error('An exception occurred' . $exception->getMessage());
            return errorMessage('An exception occurred' . $exception->getMessage());
        } finally {
            $connection->close();
        }
        return $binary_data;
    }

    /**
     * 获取modbus-tcp的值,根据数据类型进行获取
     * @param int $unitID 站号
     * @param array $data_array 数据集合
     * @return string |array
     */
    public function getModbusTcpTypeRead(array $data_array, int $unitID = 1)
    {

        if (empty($data_array)) {
            return errorMessage("数据有误");
        }
        Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST; // set default (global) endian used for parsing data
        $data = [];
        $keyToCheck = "start_address";
        $keyType = "get_type";
        $keyName = "name";
        if (!array_key_exists($keyToCheck, $data_array[0])) {
            return errorMessage("数据中不存在键名【start_address】");
        }
        if (!array_key_exists($keyName, $data_array[0])) {
            return errorMessage("数据中不存在键名【name】");
        }
        if (!array_key_exists($keyType, $data_array[0])) {
            return errorMessage("数据中不存在键名【get_type】");
        }
        $uri = "tcp://{$this->host}:{$this->port}";
        $server = ReadRegistersBuilder::newReadHoldingRegisters($uri,$unitID);
        foreach ($data_array as $value) {
            $start_address = $value['start_address'];
            $get_type = $value['get_type'];
            $name = $value['name'];
            switch ($get_type) {
                case "bit":
                    if (!array_key_exists('bit', $value)) {
                        return errorMessage("数据中不存在键名【bit】");
                    }
                    $bit = $value['bit'];
                    $server = $server->bit($start_address, $bit, $name);
                    break;
                case "int16":
                    $server = $server->int16($start_address, $name);
                    break;
                case "uint16":
                    $server = $server->uint16($start_address, $name);
                    break;
                case "int32":
                    $server = $server->int32($start_address, $name);
                    break;
                case "uint32":
                    $server = $server->uint32($start_address, $name);
                    break;
                case "uint64":
                    $server = $server->uint64($start_address, $name);
                    break;
                case "float":
                    $server = $server->float($start_address, $name);
                    break;
                case "string":
                    $server = $server->string($start_address, $name);
                    break;
                default:
                    $server = $server->byte($start_address, $name);
                    break;
            }
        }
        $requests = $server->build();
        try {
            foreach ($requests as $request) {
                $client = new NonBlockingClient(['readTimeoutSec' => 0.5, NonBlockingClient::OPT_FLAT_REQUEST_RESPONSE => true]);
                $response = $client->sendRequest($request);
                $data = $response->getData();
            }
            return $data;
        } catch (\Exception $exception) {
            return $exception->getMessage();
        }
    }

    /**
     * 写入modbus-tcp的值,根据数据类型进行写入
     * @param int $unitID 站号
     * @param array $data_array 数据集合
     * @return string |array
     */
    public function getModbusTcpTypeWrite(array $data_array, int $unitID = 1)
    {

        if (empty($data_array)) {
            return errorMessage("数据有误");
        }
        Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST; // set default (global) endian used for parsing data
        $data = [];
        $keyToCheck = "start_address";
        $keyType = "get_type";
        $keyValue = "value";
        if (!array_key_exists($keyToCheck, $data_array[0])) {
            return errorMessage("数据中不存在键名【start_address】");
        }
        if (!array_key_exists($keyValue, $data_array[0])) {
            return errorMessage("数据中不存在键名【value】");
        }
        if (!array_key_exists($keyType, $data_array[0])) {
            return errorMessage("数据中不存在键名【get_type】");
        }
        $uri = "tcp://{$this->host}:{$this->port}";
        $server = WriteRegistersBuilder::newWriteMultipleRegisters($uri,$unitID);
        foreach ($data_array as $value) {
            $start_address = $value['start_address'];
            $get_type = $value['get_type'];
            $_value = $value['value'];
            switch ($get_type) {
                case "bit":
                case "byte":
                    return errorMessage("类型不存在【bit,byte】");
                case "int16":
                    $server = $server->int16($start_address,$_value);
                    break;
                case "uint16":
                    $server = $server->uint16($start_address,$_value);
                    break;
                case "int32":
                    $server = $server->int32($start_address, $_value);
                    break;
                case "uint32":
                    $server = $server->uint32($start_address, $_value);
                    break;
                case "uint64":
                    $server = $server->uint64($start_address, $_value);
                    break;
                case "int64":
                    $server = $server->int64($start_address, $_value);
                    break;
                case "float":
                    $server = $server->float($start_address, $_value);
                    break;
                case "double":
                    $server = $server->double($start_address, $_value);
                    break;
                default:
                    $server = $server->string($start_address, $_value);
                    break;
            }
        }
        $requests = $server->build();
        try {
            foreach ($requests as $request) {
                $client = new NonBlockingClient(['readTimeoutSec' => 0.5, NonBlockingClient::OPT_FLAT_REQUEST_RESPONSE => true]);
                $response = $client->sendRequest($request);
                $data = $response->getData();
            }
            return $data;
        } catch (\Exception $exception) {
            return $exception->getMessage();
        }
    }

    /**
     * 批量读取modbus-tcp的值,根据数据类型进行读取
     * @param array $data_array
     * @param int $unitID
     * @return array|string|Response
     */
    public function getModbusTcpTypeMultipleRead(array $data_array, int $unitID = 1)
    {
        if (empty($data_array)) {
            return errorMessage("数据有误");
        }
        Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST;
        $data = [];
        $keyToCheck = "start_address";
        $keyType = "get_type";
        $name = "name";
        $uri="uri";
        if (!array_key_exists($keyToCheck, $data_array[0])) {
            return errorMessage("数据中不存在键名【start_address】");
        }
        if (!array_key_exists($name, $data_array[0])) {
            return errorMessage("数据中不存在键名【name】");
        }
        if (!array_key_exists($uri, $data_array[0])) {
            return errorMessage("数据中不存在键名【uri】");
        }
        if (!array_key_exists($keyType, $data_array[0])) {
            return errorMessage("数据中不存在键名【get_type】");
        }
        foreach ($data_array as $value) {
            $start_address = $value['start_address'];
            $uri = $value['uri'];
            //字符串是否包含tcp
            if (strpos($uri, "tcp://")){
                var_dump(20);
                return errorMessage("uri格式错误");
            }
            $server = ReadRegistersBuilder::newReadHoldingRegisters($uri,$unitID);
            $get_type = $value['get_type'];
            $name = $value['name'];
            switch ($get_type) {
                case "bit":
                    if (!array_key_exists('bit', $value)) {
                        return errorMessage("数据中不存在键名【bit】");
                    }
                    $bit = $value['bit'];
                    $server = $server->bit($start_address, $bit, $name);
                    break;
                case "byte":
                    $byte = $value['byte'];
                    $server = $server->byte($start_address,$byte,$name);
                    break;
                case "int16":
                    $server = $server->int16($start_address, $name);
                    break;
                case "uint16":
                    $server = $server->uint16($start_address, $name);
                    break;
                case "int32":
                    $server = $server->int32($start_address, $name);
                    break;
                case "uint32":
                    $server = $server->uint32($start_address, $name);
                    break;
                case "uint64":
                    $server = $server->uint64($start_address, $name);
                    break;
                case "float":
                    $server = $server->float($start_address, $name);
                    break;
                case "string":
                    $server = $server->string($start_address, $name);
                    break;
                default:
                    $server = $server->byte($start_address, $name);
                    break;
            }
            $requests = $server->build();
            try {
                foreach ($requests as $request) {
                    $client = new NonBlockingClient(['readTimeoutSec' => 0.5, NonBlockingClient::OPT_FLAT_REQUEST_RESPONSE => true]);
                    $response = $client->sendRequest($request);
                    $getData = $response->getData();
                    foreach ($getData as $k=>$v){
                        $data[$k] = $v;
                    }
                    //$data[] = $response->getData();
                }
            } catch (\Exception $exception) {
                //$this->getModbusTcpTypeMultipleRead($data_array);
                return $exception->getMessage();
            }
        }
        return $data;
    }


    /**
     * tcp与rtu的读取
     * @param array $data_array
     * @param int $quantity
     * @param int $unitID RTU数据包从属id等效于Modbus TCP单元id
     * @return array|mixed[]|Response|string
     */
    public function getTcpRtu(array $data_array,int $quantity,int $unitID)
    {
        if (empty($data_array)) {
            return errorMessage("数据有误");
        }
        Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST; // set default (global) endian used for parsing data
        $data = [];
        $keyToCheck = "start_address";
        $keyType = "get_type";
        $keyName = "name";
        if (!array_key_exists($keyToCheck, $data_array[0])) {
            return errorMessage("数据中不存在键名【start_address】");
        }
        if (!array_key_exists($keyName, $data_array[0])) {
            return errorMessage("数据中不存在键名【name】");
        }
        if (!array_key_exists($keyType, $data_array[0])) {
            return errorMessage("数据中不存在键名【get_type】");
        }
        $connection = BinaryStreamConnection::getBuilder()
            ->setPort($this->port)
            ->setHost($this->host)
            ->setReadTimeoutSec(0.5)
            ->setIsCompleteCallback(function ($binaryData, $streamIndex) {
                return Packet::isCompleteLengthRTU($binaryData);
            })
            ->setLogger(new EchoLogger())
            ->build();

        $server =  ReadRegistersBuilder::newReadHoldingRegisters('no_address',$unitID);
        foreach ($data_array as $value) {
            $start_address = $value['start_address'];
            $get_type = $value['get_type'];
            $name = $value['name'];
            switch ($get_type) {
                case "bit":
                    if (!array_key_exists('bit', $value)) {
                        return errorMessage("数据中不存在键名【bit】");
                    }
                    $bit = $value['bit'];
                    $server = $server->bit($start_address, $bit, $name);
                    break;
                case "int16":
                    $server = $server->int16($start_address, $name);
                    break;
                case "uint16":
                    $server = $server->uint16($start_address, $name);
                    break;
                case "int32":
                    $server = $server->int32($start_address, $name);
                    break;
                case "uint32":
                    $server = $server->uint32($start_address, $name);
                    break;
                case "uint64":
                    $server = $server->uint64($start_address, $name);
                    break;
                case "float":
                    $server = $server->float($start_address, $name);
                    break;
                case "string":
                    $server = $server->string($start_address, $name);
                    break;
                default:
                    $server = $server->byte($start_address, $name);
                    break;
            }
        }
        $requests = $server->build();
        try {
            foreach ($requests as $request) {
                $rtuPacket = RtuConverter::toRtu($request->getRequest());
                $binaryData = $connection->connect()->sendAndReceive($rtuPacket);
                $tcpResponsePacket = RtuConverter::fromRtu($binaryData);
                $data[] = $request->parse($tcpResponsePacket);
            }
            return $data;
        } catch (\Exception $exception) {
            return $exception->getMessage();
        }
    }
}
